<?php
/**
 * crcl/base
 *
 * Copyright 2013 CRCL - André Sabosch - info@crcl.de
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace crcl\base\template\container;

interface ITxt
{
  /**
   * Returns compiled template
   *
   * @return string
   */
  public function compile();
}

class ContainerTxt extends \crcl\base\xml\sax\Builder implements ITxt, IConcrete
{
  const TAG_VIRTUAL = 'virtual';

  public function __construct()
  {
    $this->oTriggers = new \crcl\base\template\Triggers();
  }

  public function setContent($sContent)
  {
    $this->setXml($sContent);
  }

  public function compile()
  {
    $this->parse();
    $sContent = '<?php $fExecuter = function(\crcl\base\template\Values $oValues) { ' .
                '$sContent =' . "\n" . '<<<BASE' . "\n";
    $sContent .= $this->getBuild();
    $sContent .= xhtml\instructions\Concrete::endHeredoc() . 'return $sContent; } ?>';

    return $sContent;
  }

  public function compileInclude($aValues)
  {
    $this->parse();
    $sContent = '';
    if (!empty($aValues))
    {
      $sValues = \var_export($aValues, \TRUE);
      $sContent = "\n" . 'BASE;' . "\n" .'$oValues->merge(' . $sValues  . ');' .
                  '$sContent .=' . "\n" . '<<<BASE' . "\n";
    }
    $sContent .= $this->getBuild();
    return $sContent;
  }

  //parser events (@see sax/Builder)
  public function onProcessingInstruction($sInstruction)
  {
    $sReturn = $sInstruction;
    if (\substr($sInstruction, 0, 6) == '<?base')
    {
      $aAttributes = $this->getAttributes($sInstruction);

      try
      {
        if (isset(\array_keys($aAttributes)[0]))
        {
          $oInstruction = xhtml\Instruction::factory(\array_keys($aAttributes)[0]);
        }
        else
        {
          throw new Exception('Invalid attribute',
              Exception::ERROR_COMPILER_INVALIDATTRIBUTE,
              array('aAttributes' => $aAttributes));
        }
      }
      catch (\crcl\base\instance\Exception $e)
      {
        if ($e->getCode() == $e::ERROR_INVALIDTYPE)
        {
          throw new Exception('Invalid attribute',
              Exception::ERROR_COMPILER_INVALIDATTRIBUTE,
              array('aAttributes' => $aAttributes));
        }
        else
        {
          throw $e;
        }
      }

      $oInstruction->setAttributes($aAttributes);
      $oInstruction->verify();

      $oTrigger = $oInstruction->start();
      $oTrigger->executeCurrent();
      $sReturn = $oTrigger->getContent();

      if ($oTrigger->isNext() || $oTrigger->isClose())
      {
        $this->oTriggers->add($oTrigger);
      }
    }

    parent::onProcessingInstruction($sReturn);
  }

  public function onElementStart($sTagName, $aAttributes, $bShort = \FALSE)
  {
    if ($sTagName == 'script')
    {
      $this->bScript = \TRUE;
    }

    if ($this->oTriggers->hasTrigger())
    {
      $aLoop = clone $this->oTriggers; //we should not iterate and change the same array :)
      foreach ($aLoop as $key => $oTrigger)
      {
        $this->oTriggers->remove($key);

        if ($oTrigger->isNext()) //just this tag
        {
          $oTrigger->executeNext($sTagName, $aAttributes);
          $aAttributes = $oTrigger->getAttributes();
        }

        if ($oTrigger->isClose() || $oTrigger->isScan())
        {
          if (!$oTrigger->hasTag())
          {
            $oTrigger->rememberTag($sTagName);
          }
          if ($oTrigger->hasTag($sTagName))
          {
            $oTrigger->incNestedCounter();
          }

          $this->oTriggers->add($oTrigger, $key);
        }

        if ($oTrigger->isScan())
        {
          $oTrigger->executeScan($sTagName, $aAttributes);
          $aAttributes = $oTrigger->getAttributes();
          $sTagName .= $oTrigger->getContentInTag();
        }
      }
    }

    $aAttributes = $this->compressAttributes($aAttributes);
    if ($sTagName !== self::TAG_VIRTUAL)
    {
      parent::onElementStart($sTagName, $aAttributes, $bShort);
    }
  }

  public function onElementClose($sTagName)
  {
    if ($sTagName == 'script')
    {
      $this->bScript = \FALSE;
    }

    if ($sTagName !== self::TAG_VIRTUAL)
    {
      parent::onElementClose($sTagName);
    }

    $sOutput = '';
    if ($this->oTriggers->hasTrigger())
    {
      foreach ($this->oTriggers as $key => $oTrigger)
      {
        if ($oTrigger->isClose() || $oTrigger->isScan())
        {
          if ($oTrigger->hasTag($sTagName))
          {
            $this->oTriggers->remove($key);

            if ($oTrigger->getNestedCounter() == 1)
            {
              $oTrigger->executeClose($sTagName);
              $sOutput .= $oTrigger->getContentClose();
            }
            else
            {
              $oTrigger->decNestedCounter();
              $this->oTriggers->add($oTrigger);
            }
          }
        }
      }
    }
    if ($sOutput !== '')
    {
      $this->extendBuild($sOutput);
    }
  }

  /** PRIVATE *************************************************************************************/

  private $oTriggers = \NULL;

  private $bScript = \FALSE;

  private function getAttributes($sInstruction)
  {
    $sInstruction = \str_replace('?>', '', $sInstruction);
    $sInstruction = \str_replace('<?base', '', $sInstruction);
    $sInstruction = \trim($sInstruction);

    $bAttribute = \TRUE;
    $bValue = \FALSE;
    $sAttribute = '';
    $sValue = '';
    $aReturn = array();
    for ($i=0; $i<\strlen($sInstruction); $i++)
    {
      if ($bValue)
      {
        if ($sInstruction[$i] == '"') // end of value
        {
          $bValue = \FALSE;
          $aReturn[$sAttribute] = \trim($sValue);
          continue;
        }
        else
        {
          $sValue .= $sInstruction[$i];
        }
      }
      if (!$bValue && ($sInstruction[$i] == ' ')) //new attribute
      {
        $bAttribute = \TRUE;
        $bValue = \FALSE;
        $sAttribute = '';
        $sValue = '';
        continue;
      }
      if ($sInstruction[$i] == '=')
      {
        $bAttribute = \FALSE;
        continue;
      }
      if ($bAttribute)
      {
        $sAttribute .= $sInstruction[$i];
      }
      if (!$bAttribute)
      {
        if ($sInstruction[$i] == '"') //start of value
        {
          $bValue = \TRUE;
        }
        else
        {
          if (!$bValue) //missing "
          {
            throw new Exception('Missing quotation marks',
                Exception::ERROR_COMPILER_MISSING_QUOTATIONMARKS,
                array('sProcessingInstruction' => $sInstruction, 'sAttribute' => $sAttribute));
          }
        }
      }
    }
    return $aReturn;
  }

  private function compressAttributes($aAttributes)
  {
    foreach ($aAttributes as $key => $value)
    {
      if (empty($value))
      {
        unset($aAttributes[$key]);
      }
      else
      {
        $aAttributes[$key] = \trim($aAttributes[$key]);
      }
    }
    return $aAttributes;
  }
}